/**
 * @author Vitor Franco do Carmo (vitor.carmo@capgemini.com)
 * @since 08/05/2013
 *
 */
package br.com.vfcfw.controller.model;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.faces.model.DataModel;

import org.hibernate.Criteria;
import org.hibernate.Query;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Order;
import org.primefaces.model.LazyDataModel;
import org.primefaces.model.SelectableDataModel;
import org.primefaces.model.SortOrder;

import br.com.vfcfw.business.service.IBaseService;
import br.com.vfcfw.commons.domain.business.BaseEntityBusiness;
import br.com.vfcfw.commons.domain.entity.BaseEntity;
import br.com.vfcfw.commons.domain.filter.BaseFilter;
import br.com.vfcfw.commons.exception.BusinessException;
import br.com.vfcfw.commons.util.ReflexaoUtil;
import br.com.vfcfw.commons.util.SQLText;
import br.com.vfcfw.commons.util.ValidacaoUtil;

/**
 * Abstração com a implementação padrão do {@link DataModel} usado na paginação por demanda.
 * 
 * @author Vitor Franco do Carmo (vitor.carmo@capgemini.com)
 * @since 08/05/2013
 * 
 */
public class BasePrimefacesDataModel<E extends BaseEntity, B extends E> extends LazyDataModel<B> implements
    SelectableDataModel<B> {

    /** */
    private static final long serialVersionUID = -7737366084604457586L;

    protected String campoOrdenacao;

    protected boolean filtroPorCriteria;

    protected int inicio;

    protected int qtdRegistros;

    private BaseFilter<E> filter;

    private Class<B> entityBusiness;

    private List<B> data;

    private SortOrder sortOrder;

    protected List<Serializable> wrappedKeys = new ArrayList<Serializable>();

    protected Map<Serializable, B> wrappedData = new HashMap<Serializable, B>();

    /**
     * {@link BaseService} Classe de negócio da entidade principal do Managed Bean.
     */
    protected IBaseService<E> entityService;

    /**
     * Construtor da classe.
     * 
     * @param entityClass Classe da entidade principal do {@link DataModel}.
     * @param entityBusinessClass Classe da entidade principal do {@link DataModel}.
     * @param entityService {@link BaseService} Classe de negócio da entidade principal do {@link DataModel}.
     * @param filter {@link BaseFilter} Filtro de pesquisa.
     * @param filtroPorCriteria <tt>TRUE</tt> o {@link DataModel} usará {@link Criteria} para realizar a pesquisa,
     *        <tt>FALSE</tt> usará {@link Query}.
     * @param campoOrdenacao Campo que será usado na ordenação.
     */
    public BasePrimefacesDataModel(Class<E> entityClass, Class<B> entityBusinessClass, IBaseService<E> entityService,
        BaseFilter<E> filter, boolean filtroPorCriteria, String campoOrdenacao) {

        super();
        this.entityService = entityService;
        this.filter = filter;
        this.entityBusiness = entityBusinessClass;
        this.filtroPorCriteria = filtroPorCriteria;
        this.campoOrdenacao = campoOrdenacao;

        this.data = obterPorFiltro(possuiValor(campoOrdenacao) ? campoOrdenacao : this.campoOrdenacao,
            SortOrder.UNSORTED, inicio, qtdRegistros);
        setWrappedData(data);
    }

    /**
     * Construtor da classe.
     * 
     * @param dados {@link List} Lista com os dados da pesquisa.
     * @param entityClass Classe da entidade principal do {@link DataModel}.
     * @param entityBusinessClass Classe da entidade principal do {@link DataModel}.
     * @param entityService {@link IBaseService} Classe de negócio da entidade principal do {@link DataModel}.
     * @param filter {@link BaseFilter} Filtro de pesquisa.
     * @param filtroPorCriteria <tt>TRUE</tt> o {@link DataModel} usará {@link Criteria} para realizar a pesquisa,
     *        <tt>FALSE</tt> usará {@link Query}.
     * @param campoOrdenacao Campo que será usado na ordenação.
     */
    public BasePrimefacesDataModel(List<B> dados, Class<E> entityClass, Class<B> entityBusinessClass,
        IBaseService<E> entityService, BaseFilter<E> filter, boolean filtroPorCriteria, String campoOrdenacao) {

        this(entityClass, entityBusinessClass, entityService, filter, filtroPorCriteria, campoOrdenacao);
        this.data = dados;
        setWrappedData(data);

        for (B entity : dados) {
            wrappedKeys.add(entity.getChavePrimaria());
            String pk = (possuiValor(entity.getChavePrimaria())) ? entity.getChavePrimaria().toString() : null;
            wrappedData.put(pk, entity);
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.primefaces.model.LazyDataModel#load(int, int, java.lang.String, org.primefaces.model.SortOrder,
     *      java.util.Map)
     */
    @Override
    public List<B> load(int inicio, int qtdRegistros, String campoOrdenacao, SortOrder ordemClassificacao,
        Map<String, String> filters) {

        this.data = obterPorFiltro(possuiValor(campoOrdenacao) ? campoOrdenacao : this.campoOrdenacao,
            ordemClassificacao, inicio, qtdRegistros);

        wrappedKeys = new ArrayList<Serializable>();

        if (possuiValor(data)) {

            for (B entity : data) {

                wrappedKeys.add(entity.getChavePrimaria());
                String pk = (possuiValor(entity.getChavePrimaria())) ? entity.getChavePrimaria().toString() : null;
                wrappedData.put(pk, entity);
            }
        }

        setRowCount(obterQuantidadeRegistros());

        this.inicio = inicio;
        this.qtdRegistros = qtdRegistros;
        if (possuiValor(campoOrdenacao)) {
            this.campoOrdenacao = campoOrdenacao;
        }
        this.sortOrder = ordemClassificacao;

        return data;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.primefaces.model.LazyDataModel#getRowData(java.lang.String)
     */
    @Override
    public B getRowData(String rowKey) {

        B entityBusiness = null;

        if (possuiValor(rowKey)) {

            entityBusiness = (B) wrappedData.get(rowKey);
        }
        return entityBusiness;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.primefaces.model.LazyDataModel#getRowKey(java.lang.Object)
     */
    @Override
    public Object getRowKey(B object) {

        return object.getChavePrimaria();
    }

    /**
     * Consulta uma entidade pelo filtro no banco usando paginação.
     * 
     * @param campoOrdenacao Nome do campo de ordenação.
     * @param ordemClassificacao {@link SortOrder} Ordem de classificação.
     * @param inicio Início da consulta.
     * @param qtdRegistros Quantidade máxima de registros que podem ser retornados.
     * @return Lista de objetos da entidade mapeada com JPA que atendem ao filtro.
     */
    @SuppressWarnings("unchecked")
    protected List<B> obterPorFiltro(String campoOrdenacao, SortOrder ordemClassificacao, int inicio, int qtdRegistros) {

        if (this.data == null || isPaginaAlterada(campoOrdenacao, ordemClassificacao, inicio, qtdRegistros)) {

            if (isFiltroPorCriteria()) {
                this.data = (List<B>) entityService.consultarPorCriterio(
                    criarConsultaPorCriteria(campoOrdenacao, ordemClassificacao), inicio, qtdRegistros);

            } else if (possuiValor(filter.getNativeSQL())) {

                try {

                    traduzirResultado(entityService.consultarPorFiltroNativo(
                        criarConsultaPorQueryNativa(campoOrdenacao, ordemClassificacao), filter.getProjecao(), inicio,
                        qtdRegistros));
                } catch (Exception e) {
                    throw new BusinessException(e);
                }

            } else {
                this.data = (List<B>) entityService.consultarPorFiltro(
                    criarConsultaPorQuery(campoOrdenacao, ordemClassificacao), inicio, qtdRegistros);
            }
        }
        return this.data;
    }

    /**
     * Recebe lista com Vetor de objetos e traduz para uma Entity.
     * 
     * @param resultado Lista com o vetor de objetos.
     * @throws Exception
     */
    private void traduzirResultado(List<Map<String, Object>> resultado) throws Exception {

        if (resultado != null && !resultado.isEmpty()) {
            data = new ArrayList<B>();

            B retorno = null;

            for (Map<String, Object> linha : resultado) {

                retorno = ReflexaoUtil.obterInstancia(getEntityBusiness());

                for (Entry<String, Object> projecao : getFilter().getProjecao().entrySet()) {

                    ReflexaoUtil.executarMetodoSet(getEntityBusiness(), projecao.getKey(), retorno,
                        linha.get(projecao.getKey()));
                }
                data.add(retorno);
            }
        }
    }

    /**
     * Obtém a quantidade de registros baseado nos critérios de pesquisa.
     * 
     * @param detachedCriteria {@link DetachedCriteria} Critérios de pesquisa.
     * @return Quantidade de registros.
     */
    protected int obterQuantidadeRegistros() {

        Integer qtdRegistros = entityService.obterQuantidadeRegistros(this.getFilter(), this.isFiltroPorCriteria());

        return qtdRegistros.intValue();
    }

    /**
     * Monta o critério de pesquisa com os atributos de ordenação.
     * 
     * @param sortField Campo de ordenação.
     * @param sortOrder Ordem de classificação.
     * 
     * @return Critérios de pesquisa.
     */
    protected DetachedCriteria criarConsultaPorCriteria(String campoOrdenacao, SortOrder ordemClassificacao) {

        DetachedCriteria detachedCriteria = filter.getDetachedCriteria();

        List<Order> ordensClassificacao = criarOrdensClassificacaoParaCriteria(campoOrdenacao, ordemClassificacao);

        for (Order ordem : ordensClassificacao) {

            detachedCriteria.addOrder(ordem);
        }
        return detachedCriteria;
    }

    /**
     * Cria a lista, com os atributos de ordenação, que será usada na pesquisa ordenada.
     * 
     * @param campoOrdenacao Campo de ordenação.
     * @param ordemClassificacao Ordem de classificação.
     * 
     * @return Lista com os atributos de ordenação.
     */
    protected List<Order> criarOrdensClassificacaoParaCriteria(String campoOrdenacao, SortOrder ordemClassificacao) {

        List<Order> ordensClassificacao = new ArrayList<Order>();

        Order ordem = null;

        if (possuiValor(campoOrdenacao)) {

            if (ordemClassificacao.equals(SortOrder.ASCENDING)) {
                ordem = Order.asc(campoOrdenacao);
                ordensClassificacao.add(ordem);
            } else if (ordemClassificacao.equals(SortOrder.DESCENDING)) {
                ordem = Order.desc(campoOrdenacao);
                ordensClassificacao.add(ordem);
            }
        }
        return ordensClassificacao;
    }

    /**
     * Cria a consulta por query nativa com os atributos de ordenação.
     * 
     * @param campoOrdenacao Campo de ordenação.
     * @param ordemClassificacao Ordem de classificação.
     * 
     * @return {@link SQLText} Consulta SQL.
     */
    protected SQLText criarConsultaPorQueryNativa(String campoOrdenacao, SortOrder ordemClassificacao) {

        SQLText sql = new SQLText();

        sql.adicionarSentencaSQLCompleta(filter.getNativeSQL());

        Map<String, String> ordensClassificacao = criarOrdensClassificacaoParaQuery(campoOrdenacao, ordemClassificacao);

        if (!ordensClassificacao.isEmpty()) {
            sql.adicionar(" ORDER BY ");
        }

        boolean primeiraOrdenacao = true;

        for (Entry<String, String> entry : ordensClassificacao.entrySet()) {

            if (!primeiraOrdenacao) {

                sql.adicionar(", ");
            }
            sql.adicionar(entry.getKey()).adicionar(" ").adicionar(entry.getValue());
        }
        return sql;
    }

    /**
     * Cria a consulta por query com os atributos de ordenação.
     * 
     * @param campoOrdenacao Campo de ordenação.
     * @param ordemClassificacao Ordem de classificação.
     * 
     * @return {@link SQLText} Consulta SQL.
     */
    protected SQLText criarConsultaPorQuery(String campoOrdenacao, SortOrder ordemClassificacao) {

        SQLText sql = new SQLText();

        sql.adicionarSentencaSQLCompleta(filter.getSQL());

        Map<String, String> ordensClassificacao = criarOrdensClassificacaoParaQuery(campoOrdenacao, ordemClassificacao);

        if (!ordensClassificacao.isEmpty()) {
            sql.adicionar("ORDER BY ");
        }

        boolean primeiraOrdenacao = true;

        for (Entry<String, String> entry : ordensClassificacao.entrySet()) {

            if (!primeiraOrdenacao) {

                sql.adicionar(", ");
            }
            sql.adicionar(entry.getKey()).adicionar(" ").adicionar(entry.getValue());
        }
        return sql;
    }

    /**
     * Cria um mapa, com os atributos de ordenação, que será usado na construção da pesquisa.
     * 
     * @param campoOrdenacao Campo de ordenação.
     * @param ordemClassificacao Ordem de classificação.
     * 
     * @return Lista com os atributos de ordenação.
     */
    protected Map<String, String> criarOrdensClassificacaoParaQuery(String campoOrdenacao, SortOrder ordemClassificacao) {

        Map<String, String> ordensClassificacao = new HashMap<String, String>();

        if (possuiValor(campoOrdenacao)) {
            if (ordemClassificacao == SortOrder.ASCENDING) {
                ordensClassificacao.put(campoOrdenacao, "ASC");
            } else if (ordemClassificacao == SortOrder.DESCENDING) {
                ordensClassificacao.put(campoOrdenacao, "DESC");
            }
        }
        return ordensClassificacao;
    }

    /**
     * Verifica se a página foi alterada.
     * 
     * @param campoOrdenacao Campo de classificação.
     * @param ordemClassificacao {@link SortOrder} Ordem de classificação.
     * @param inicio Início da consulta.
     * @param qtdRegistros Quantidade máxima de registros que podem ser retornados.
     * @return <tt>TRUE</tt> se a página foi alterada, <tt>FALSE</tt> caso contrário.
     */
    protected boolean isPaginaAlterada(String campoOrdenacao, SortOrder ordemClassificacao, int inicio, int qtdRegistros) {

        if (this.campoOrdenacao == null) {
            this.campoOrdenacao = "";
        }

        return (this.inicio != inicio)
            || (this.qtdRegistros != qtdRegistros)
            || (!this.campoOrdenacao.equals(campoOrdenacao))
            || (this.sortOrder != ordemClassificacao);
    }

    /**
     * Retorna o valor do atributo filtroPorCriteria.
     * 
     * @return Valor do atributo filtroPorCriteria.
     */
    public boolean isFiltroPorCriteria() {

        return filtroPorCriteria;
    }

    /**
     * Especifica o valor do atributo filtroPorCriteria.
     * 
     * @param filtroPorCriteria boolean do filtroPorCriteria a ser setado.
     */
    public void setFiltroPorCriteria(boolean filtroPorCriteria) {

        this.filtroPorCriteria = filtroPorCriteria;
    }

    /**
     * Retorna o valor do atributo filter.
     * 
     * @return Valor do atributo filter.
     */
    public BaseFilter<E> getFilter() {

        return filter;
    }

    /**
     * Especifica o valor do atributo filter.
     * 
     * @param filter BaseFilter<E> do filter a ser setado.
     */
    public void setFilter(BaseFilter<E> filter) {

        this.filter = filter;
    }

    /**
     * Retorna o valor do atributo entityBusiness
     * 
     * @return Valor do atributo entityBusiness
     */
    public Class<B> getEntityBusiness() {

        return entityBusiness;
    }

    /**
     * Especifica o valor do atributo entity Business
     * 
     * @param entityBusiness {@link BaseEntityBusiness} do business a ser setado
     */
    public void setEntityBusiness(Class<B> entityBusiness) {

        this.entityBusiness = entityBusiness;
    }

    /**
     * Verifica se o valor não é nulo ou vazio.
     * 
     * @param valor Valor.
     * @return <tt>TRUE</tt> se não for <b>nulo</b> ou <b>vazio</b>, <tt>FALSE</tt> caso contrário.
     */
    protected boolean possuiValor(Object valor) {

        return ValidacaoUtil.possuiValor(valor);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.primefaces.model.LazyDataModel#setRowIndex(int)
     */
    @Override
    public void setRowIndex(int rowIndex) {

        super.setRowIndex(getPageSize() == 0 ? -1 : rowIndex);
    }

    /**
     * Verifica se o {@link BasePrimefacesDataModel} retornou registros.
     * 
     * @return <tt>TRUE</tt> se retornou, <tt>FALSE</tt> caso contrário.
     */
    public boolean possuiRegistros() {

        return (data != null && !data.isEmpty());
    }
}
